(ns zesql.core
  (:require [yesql.generate :as ys :refer [insert-handler execute-handler query-handler
                                           generate-var expected-parameter-list]]
            [yesql.util :refer [slurp-from-classpath]]
            [yesql.queryfile-parser :refer [parse-tagged-queries]]
            [clojure.string :refer [join lower-case]]
            [zesql.support.selmer :refer [render]]
            [zesql.tags]))

;; dynamic parser, default true
(defonce dynamic? (atom true))

(defn dynamic-on! []
  (reset! dynamic? true))

(defn dynamic-off! []
  (reset! dynamic? false))

;; debug, default false
(defonce debug? (atom false))

(defn debug-on! []
  (reset! debug? true))

(defn debug-off! []
  (reset! debug? false))

;; cache, default false
(defonce cache? (atom false))

(defn cache-on! []
  (reset! cache? true))

(defn cache-off! []
  (reset! cache? false))

;; tags
(def ^:private default-tags {:tag-open     \{
                             :tag-close    \}
                             :filter-open  \{
                             :filter-close \}
                             :tag-second   \%})
(defonce tags (atom default-tags))

(def ^:dynamic ^:private *sql-name*)

(defn- log-sql! [s & [more]]
  (when @debug?
    (cond
      (coll? s) (do (print "\tZesql {" *sql-name* "} ===> ")
                    (pr (vec s))
                    (println (str more)))
      :else (do (print "\tZesql {" *sql-name* "} --->  ")
                (pr s)
                (println (str " " more)))))
  s)

(defmacro log [expr]
  `(if @debug?
     (let [start# (. System (nanoTime))
           ret#   ~expr
           time#  (/ (double (- (. System (nanoTime)) start#)) 1000000.0)]
       (log-sql! ret# (str "      <<Elapsed time: " time# " msecs>>"))
       ret#)
     ~expr))

(defn- parse-dynamic [query initial-args]
  (if @dynamic?
    (let [opts  {:tag-open     (or (:tag-open @tags) (:tag-open default-tags))
                 :tag-close    (or (:tag-close @tags) (:tag-close default-tags))
                 :filter-open  (or (:filter-open @tags) (:filter-open default-tags))
                 :filter-close (or (:filter-close @tags) (:filter-close default-tags))
                 :tag-second   (or (:tag-second @tags) (:tag-second default-tags))}
          query (log (render query initial-args opts))]
      query)
    query))

#_(defn- zesql-rewrite-query-for-jdbc [query initial-args]
  (-> query
    (parse-dynamic initial-args)
    (ys/rewrite-query-for-jdbc initial-args)))

(defn- zesql-rewrite-query-for-jdbc [query initial-args]
  (as-> query q
    (parse-dynamic q initial-args)
    (log (ys/rewrite-query-for-jdbc q initial-args))))

(def ^:private memo-rewrite-query-for-jdbc (memoize zesql-rewrite-query-for-jdbc))

(defn- zesql-generate-query-fn
  "Generate a function to run a query.

  - If the query name ends in `!` it will call `clojure.java.jdbc/execute!`,
  - If the query name ends in `<!` it will call `clojure.java.jdbc/insert!`,
  - otherwise `clojure.java.jdbc/query` will be used."
  [{:keys [name docstring statement]
    :as   query}
   query-options]
  (assert name "Query name is mandatory.")
  (assert statement "Query statement is mandatory.")
  (let [jdbc-fn           (cond
                            (= (take-last 2 name) [\< \!]) insert-handler
                            (= (last name) \!) execute-handler
                            :else query-handler)
        ;jdbc-fn           (fn [& args])                           ;; for bench
        required-args     (expected-parameter-list statement)
        global-connection (:connection query-options)
        real-fn           (fn [args call-options]
                            (let [connection (or (:connection call-options) global-connection)
                                  connection (if (delay? connection) @connection connection)
                                  rewrite-fn (if @cache?
                                               memo-rewrite-query-for-jdbc
                                               zesql-rewrite-query-for-jdbc)]
                              (assert connection
                                (format (join "\n"
                                          ["No database connection supplied to function '%s',"
                                           "Check the docs, and supply {:connection ...} as an option to the function call, or globally to the defquery declaration."])
                                  name))
                              (binding [*sql-name* name]
                                (jdbc-fn connection
                                  (rewrite-fn statement args) ;; Rewrite by Zesql
                                  call-options))))
        [display-args generated-function] (let [named-args  (if-let [as-vec (seq (mapv (comp symbol clojure.core/name)
                                                                                   required-args))]
                                                              {:keys as-vec}
                                                              {})
                                                global-args {:keys ['connection]}]
                                            (if global-connection
                                              (if (empty? required-args)
                                                [(list []
                                                   [named-args] ;; Add by Zesql
                                                   [named-args global-args])
                                                 (fn query-wrapper-fn
                                                   ([] (query-wrapper-fn {} {}))
                                                   ([args] (query-wrapper-fn args {})) ;; Add by Zesql
                                                   ([args call-options] (real-fn args call-options)))]
                                                [(list []   ;; Add by Zesql
                                                   [named-args]
                                                   [named-args global-args])
                                                 (fn query-wrapper-fn
                                                   ([] (query-wrapper-fn {} {})) ;; Add by Zesql
                                                   ([args] (query-wrapper-fn args {}))
                                                   ([args call-options] (real-fn args call-options)))])
                                              [(list [named-args global-args])
                                               (fn query-wrapper-fn
                                                 ([args call-options] (real-fn args call-options)))]))]
    (with-meta generated-function
      (merge {:name     name
              :arglists display-args
              ::source  (str statement)}
        (when docstring
          {:doc docstring})))))

(defn defqueries
  "Defines several query functions, as defined in the given SQL file.
  Each query in the file must begin with a `-- name: <function-name>` marker,
  followed by optional comment lines (which form the docstring), followed by
  the query itself.

  options
    :connection - db-spec described in clojure.java.jdbc/get-connection, (delay db-spec) can also."
  ([filename]
   (defqueries filename {}))
  ([filename options]
   (with-redefs [yesql.generate/generate-query-fn zesql-generate-query-fn] ;; Add by Zesql
     (doall (->> filename
              slurp-from-classpath
              parse-tagged-queries
              (map #(generate-var % options)))))))

(defn defquery*
  [name filename options]
  ;;; TODO Now that we have a better parser, this is a somewhat suspicious way of writing this code.
  (with-redefs [yesql.generate/generate-query-fn zesql-generate-query-fn] ;; Add by Zesql
    (doall (->> filename
             slurp-from-classpath
             (format "-- name: %s\n%s" name)
             parse-tagged-queries
             (map #(generate-var % options))))))

;;; defquery is a macro solely because of the unquoted symbol it accepts
;;; as its first argument. It is tempting to deprecate defquery. There
;;; again, it makes things so easy to get started with yesql it might
;;; be worth keeping for that reason alone.
(defmacro defquery
  "Defines a query function, as defined in the given SQL file.
  Any comments in that file will form the docstring."
  ([name filename]
   `(defquery ~name ~filename {}))
  ([name filename options]
   `(defquery* ~(str name) ~filename ~options)))
